﻿using System;
using System.Linq;
using System.Reflection;

using JetBrains.Annotations;

namespace Rsdn.SmartApp
{
	/// <summary>
	/// Хелперный класс для работы с Event Broker.
	/// </summary>
	public static class EventBrokerHelper
	{
		/// <summary>
		/// Subscribe to event with delegate for OnNext.
		/// </summary>
		public static IDisposable Subscribe<T>(
			[NotNull] this IEventBroker eventBroker,
			[NotNull] string eventName,
			[NotNull] Action<T> nextAction)
		{
			if (eventBroker == null)
				throw new ArgumentNullException("eventBroker");
			if (eventName == null)
				throw new ArgumentNullException("eventName");
			if (nextAction == null)
				throw new ArgumentNullException("nextAction");

			return eventBroker.Subscribe(eventName, ObservableHelper.CreateObserver(nextAction));
		}

		/// <summary>
		/// Подписывает на события специально размеченные методы.
		/// </summary>
		public static IDisposable SubscribeEventHandlers(
			[NotNull] object instance,
			[NotNull] IServiceProvider provider)
		{
			if (instance == null)
				throw new ArgumentNullException("instance");
			if (provider == null)
				throw new ArgumentNullException("provider");

			var eventBroker = provider.GetRequiredService<IEventBroker>();
			var subscriptions = instance
				.GetType()
				.GetMethods(BindingFlags.Instance | BindingFlags.Public | BindingFlags.NonPublic)
				.SelectMany(
					method => method.GetCustomAttributes<EventHandlerAttribute>(true),
					(method, attr) =>
					{
						try
						{
							return eventBroker.SubscribeMethod(attr.EventName, method, instance);
						}
						catch (Exception ex)
						{
							throw new ApplicationException(
								"Error subscribing method '{0}'.".FormatStr(method), ex);
						}
					});

			return DisposableHelper.CreateDisposable(subscriptions);
		}

		/// <summary>
		/// Регистрирует специально размеченные источники событий.
		/// </summary>
		public static IDisposable RegisterEventSources(
			[NotNull] object instance,
			[NotNull] IServiceProvider provider)
		{
			if (instance == null)
				throw new ArgumentNullException("instance");
			if (provider == null)
				throw new ArgumentNullException("provider");

			var eventBroker = provider.GetRequiredService<IEventBroker>();

			var instanceType = instance.GetType();
			var propertyRegistrations = instanceType
				.GetProperties(BindingFlags.Instance | BindingFlags.Public | BindingFlags.NonPublic)
				.SelectMany(
					property => property.GetCustomAttributes<EventSourceAttribute>(true),
					(property, attr) =>
					{
						try
						{
							return eventBroker.RegisterProperty(attr.EventName, property, instance);
						}
						catch (Exception ex)
						{
							throw new ApplicationException(
								"Error registration property '{0}'.".FormatStr(property), ex);
						}
					});
			var eventRegistrations = instanceType
				.GetEvents(BindingFlags.Instance | BindingFlags.Public)
				.SelectMany(
					ev => ev.GetCustomAttributes<EventSourceAttribute>(true),
					(ev, attr) =>
					{
						try
						{
							return eventBroker.RegisterEvent(attr.EventName, ev, instance);
						}
						catch (Exception ex)
						{
							throw new ApplicationException(
								"Error registration event '{0}'.".FormatStr(ev), ex);
						}
					});

			return DisposableHelper.CreateDisposable(
				propertyRegistrations.Concat(eventRegistrations));
		}

		private static IDisposable SubscribeMethod(
			this IEventBroker eventBroker,
			string eventName,
			MethodInfo methodInfo,
			object instance)
		{
			var parameters = methodInfo.GetParameters();
			if (parameters.Length != 1)
				throw new ApplicationException("Method must have one argument.");

			var argType = parameters[0].ParameterType;

			var observer = typeof(ObservableHelper)
				.GetMethod("CreateObserver")
				.MakeGenericMethod(argType)
				.Invoke(
					null,
					new[]
			        {
			            Delegate.CreateDelegate(
			                typeof(Action<>).MakeGenericType(argType),
			                instance,
			                methodInfo)
			        });

			return (IDisposable)eventBroker
				.GetType()
				.GetMethod("Subscribe")
				.MakeGenericMethod(argType)
				.Invoke(eventBroker, new[] { eventName, observer });
		}

		private static IDisposable RegisterProperty(
			this IEventBroker eventBroker,
			string eventName,
			PropertyInfo property,
			object instance)
		{
			if (!property.PropertyType.IsGenericType
					|| property.PropertyType.GetGenericTypeDefinition() != typeof(IObservable<>))
				throw new ApplicationException("Return type is not IObservable<>.");

			if (!property.CanRead)
				throw new ApplicationException("Missing get accessor.");

			var observable = property.GetValue(instance, null);
			if (observable == null)
				throw new ApplicationException("Return value is null.");

			var argType = property.PropertyType.GetGenericArguments().Single();
			var observer = typeof(ObservableHelper)
				.GetMethod("CreateObserver")
				.MakeGenericMethod(argType)
				.Invoke(
					null,
					new[]
					{
						CreateFireDelegate(
							eventBroker,
							argType,
							eventName,
							typeof(Action<>).MakeGenericType(argType))
					});

			var subscription = (IDisposable)observable
				.GetType()
				.GetMethod("Subscribe")
				.Invoke(observable, new[] { observer });

			return subscription;
		}

		private static IDisposable RegisterEvent(
			this IEventBroker eventBroker,
			string eventName,
			EventInfo eventInfo,
			object instance)
		{
			var eventHandlerType = eventInfo.EventHandlerType;
			if (eventHandlerType == null)
				throw new ApplicationException();

			var parameters = eventHandlerType.GetMethod("Invoke").GetParameters();
			if (parameters.Length != 1)
				throw new ApplicationException("Delegate must have one argument.");

			var handler = CreateFireDelegate(
				eventBroker,
				parameters[0].ParameterType,
				eventName,
				eventHandlerType);

			eventInfo.AddEventHandler(instance, handler);

			return DisposableHelper.CreateDisposable(() => eventInfo.RemoveEventHandler(instance, handler));
		}

		private static Delegate CreateFireDelegate(
			IEventBroker eventBroker, Type argType, string eventName, Type delegateType)
		{
			var fireMethod = eventBroker
				.GetType()
				.GetMethod("Fire")
				.MakeGenericMethod(argType);

			var nextActionDelegate = (Delegate)typeof(EventBrokerHelper)
				.GetMethod("CreateGenericAction", BindingFlags.NonPublic | BindingFlags.Static)
				.MakeGenericMethod(argType)
				.Invoke(
					null,
					new[] { (Action<object>)(arg => fireMethod.Invoke(eventBroker, new[] { eventName, arg })) });

			return Delegate.CreateDelegate(
				delegateType,
				nextActionDelegate.Target,
				nextActionDelegate.Method);
		}

		// ReSharper disable UnusedMember.Local
		private static Action<T> CreateGenericAction<T>(Action<object> action)
		// ReSharper restore UnusedMember.Local
		{
			return arg => action(arg);
		}
	}
}